﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.ComponentModel;
using System.Diagnostics;
using MoneyWentWhere.DataModel.Binding;

namespace MoneyWentWhere.DataModel
{
    [DataContract]
    [DebuggerDisplay("id={Guid} date={Date} ${Amount}")]
    public class Transaction : INotifyPropertyChanged, IExtensibleDataObject
    {
        Account _account;
        TransactionList List { get { return _account == null ? null : _account.Transactions; } }

        // For use with DataBinding only
        public Transaction()
        {
            Date = DateTime.MaxValue;
            _kind = TransactionKind.Normal;
        }

        internal Transaction(
            Guid guid,
            Account account,
            TransactionKind kind,
            DateTime date,
            Guid transferToTxGuid,
            decimal amount)
            : this(guid, account, kind, date, transferToTxGuid, amount, "", "") { }

        internal Transaction(
            Guid guid,
            Account account,
            TransactionKind kind,
            DateTime date,
            Guid transferToTxGuid,
            decimal amount, 
            string comment,
            string category)
        {
            if (account == null) { throw new ArgumentNullException("account"); }

            _guid = guid;
            if (_guid != Guid.Empty) { account.DataSet.StoreObject(_guid, this); }
             _account = account;
            _kind = kind;
            _date = date;

            _transferToTxGuid = transferToTxGuid;            
            _amount = amount;
            _comment = comment;
            
            Category = category; // Add via the accessor
        }

        public Account Account
        {
            get { return _account; }
            internal set
            {
                if (value == _account) { return; }

                _account = value;
                _accountBalance = -1; // should be recalculated
                OnPropertyChanged("Account");
            }
        }

        [Browsable(false)]
        public MoneyDataSet DataSet
        {
            get { return _account.DataSet; }
        }

        #region Persisted Properties

        [DataMember(Name = "Guid")]
        Guid _guid = Guid.Empty;

        [DataMember(Name = "Kind")]
        TransactionKind _kind;

        [DataMember(Name = "Date")]
        DateTime _date;

        [DataMember(Name = "Amount")]
        decimal _amount;

        [DataMember(Name = "Comment")]
        String _comment = String.Empty;

        [DataMember(Name = "Category")]
        String _category = String.Empty;

        [DataMember(Name = "TransferToTx")]
        Guid _transferToTxGuid = Guid.Empty;

        public Guid Guid
        {
            get
            {
                if (_guid == Guid.Empty)
                {
                    _guid = Guid.NewGuid();
                    Account.DataSet.StoreObject(_guid, this);
                }
                return _guid;
            }
        }

        public DateTime Date 
        {
            get { return _date; }
            set
            {
                if (value == _date) { return; }
                _date = value;
                if (List != null) { List.OnDateChanged(this); }
            }
        }

        public decimal Amount 
        {
            get { return _amount; }
            set
            {
                if (value == _amount) { return; }
                decimal amountChange = value - _amount;
                SetAmount(value);
                if (List != null) { List.OnAmountChanged(this, amountChange); }

                // Change the other TX if in a transfer
                if (TransferToTx != null)
                {
                    // Update ONLY if it is in the same currency
                    if (Account.Currency == TransferToTx.Account.Currency)
                    {
                        // Will not cause an infinite loop, since we have a check on top
                        TransferToTx.Amount = -Amount;
                    }
                }

            }
        }

        /// <summary>
        /// Directly sets the amount variable, 
        /// without enforcing the reconcile rules.
        /// INTERNAL USE ONLY
        /// </summary>
        /// <param name="value"></param>
        internal void SetAmount(decimal value)
        {
            if (value == _amount) { return; }
            _amount = value;
            OnPropertyChanged("Amount");
            OnPropertyChanged("Deposit");
            OnPropertyChanged("Withdrawal");
        }

        // Proxy property
        public decimal Deposit
        {
            get { return Amount > 0 ? Amount : 0; }
            set { Amount = value; }
        }

        // Proxy property
        public decimal Withdrawal
        {
            get { return Amount < 0 ? -Amount : 0; }
            set { Amount = -value; }
        }

        public TransactionKind Kind
        {
            get { return _kind; }
            internal set 
            {
                if (value == _kind) { return; }
                TransactionKind oldKind = _kind;
                _kind = value;

                // Remove transfer when changing kind away from it
                if (oldKind == TransactionKind.Transfer)
                {
                    TransferRemove();
                }

                OnPropertyChanged("Kind");
            }
        }

        public String Category
        {
            get { return _category; }
            set
            {
                if (value == _category) { return; }
                _category = value;
                DataSet.Categories.Add(_category);
                OnPropertyChanged("Category");
            }
        }

        public String Comment
        {
            get { return _comment; }
            set
            {
                if (value == _comment) { return; }
                _comment = value;
                OnPropertyChanged("Comment");
            }
        }

        #endregion

        public TxSettings Settings
        {
            get { return TxSettings.Create(this); }
            set 
            {
                if (value == null)  { return; }                
                value.Apply(this); 
            }
        }

        /// <summary>
        /// Change the transaction to reconcile and set the amount.
        /// </summary>
        /// <param name="amount"></param>
        public void ReconcileTo(decimal amount)
        {
            Kind = TransactionKind.Reconcile;
            Amount += amount - AccountBalance;
        }


        #region Transfer handling

        // Lazily created
        Transaction _transferToTx;

        /// <summary>
        /// If this tx is a transfer, returns the tx on the other side
        /// of the transfer. Returns null if no transfer.
        /// </summary>
        public Transaction TransferToTx
        {
            get
            {
                if (_transferToTxGuid == Guid.Empty) { return null; }

                if (_transferToTx == null)
                {
                    _transferToTx = DataSet.GetObject<Transaction>(_transferToTxGuid);

                    // Few sanity checks
                    if (_transferToTx == null)
                    {
                        this.Comment = "ERROR! Transfer: Other tx not present." + this.Comment;
                        _transferToTxGuid = Guid.Empty;
                        _transferToTx = null;
                    }
                    if (_transferToTx.TransferToTx != this)
                    {
                        Console.WriteLine("ERROR! Transfer: Other tx not pointing back.");
                        this.Comment = "ERROR! Transfer Mismatch. " + this.Comment;
                        _transferToTxGuid = Guid.Empty;
                        _transferToTx = null;
                    }
                }
                return _transferToTx;
            }
            private set
            {
                // Warning -- shallow setter.
                // Do NOT make this method public, data corruption may result.

                _transferToTx = value;
                if (_transferToTx == null) { _transferToTxGuid = Guid.Empty; }
                else { _transferToTxGuid = _transferToTx.Guid; }

                OnPropertyChanged("TransferToTx");
            }
        }

        /// <summary>
        /// Create a transfer to the given account.
        /// If a transfer already exists, removes it first.
        /// </summary>
        /// <param name="toAccount"></param>
        public void TransferCreate(Account toAccount)
        {
            decimal toAmount = -Amount;
            if (Account.Currency != toAccount.Currency)
            {
                decimal xr = DataSet.GetExchangeRate(Account.Currency, toAccount.Currency, Date);
                if (xr > 0) { toAmount = -(xr * Amount); }
            }
            TransferCreate(toAccount, toAmount);
        }

        /// <summary>
        /// Create a transfer to the given account.
        /// If a transfer already exists, removes it first.
        /// </summary>
        /// <param name="toAccount"></param>
        public void TransferCreate(Account toAccount, decimal toAmount)
        {
            if (toAccount == Account)
            {
                throw new ArgumentException("Can't transfer to same acct.");
            }
            if ((Amount > 0 && toAmount > 0) ||
                (Amount < 0 && toAmount < 0))
            {
                throw new ArgumentException("Amounts must have opposite sign.");
            }
            if ((Amount == 0 && toAmount != 0) || 
                (Amount != 0 && toAmount == 0))
            {
                throw new ArgumentException("Amounts must either be both zero or both non-zero.");
            }

            if (TransferToTx != null)
            {
                // Already have a transfer to given account, no need to remove+add
                if (toAccount == TransferToTx.Account) 
                {
                    TransferToTx.Amount = toAmount; // Adjust amount
                    return; 
                }

                // Remove the existing transfer so we can make a new one
                TransferRemove();
            }

            Kind = TransactionKind.Transfer;

            // Create and link up the other Tx
            TransferToTx = toAccount.CreateTransaction(
                Date, TransactionKind.Transfer, toAmount);
            TransferToTx.TransferToTx = this;
        }

        /// <summary>
        /// Remove the transfer, reverting the transaction to Normal.
        /// Deletes the other transaction in the transfer.
        /// If there was no transfer to remove, returns false.
        /// </summary>
        /// <param name="tx"></param>
        public bool TransferRemove()
        {
            if (TransferToTx == null) { return false; }

            // Remove other Tx
            TransferToTx.TransferToTx = null;
            TransferToTx.Kind = TransactionKind.Normal;
            TransferToTx.Delete();

            // Revert this Tx to normal
            Kind = TransactionKind.Normal;
            TransferToTx = null;
            return true;
        }

        #endregion

        #region Computed Properties

        decimal _accountBalance;

        /// <summary>
        /// Gets the balance in the account at the time of transaction.
        /// BEWARE: setting this converts the tx into a Reconcile.
        /// </summary>
        public decimal AccountBalance
        {
            get { return _accountBalance; }
            set
            {
                if (value == _accountBalance) { return; }
                if (Kind != TransactionKind.Normal &&
                    Kind != TransactionKind.Reconcile)
                {
                    throw new InvalidOperationException("Can only change balance on Normal/Reconcile tx.");
                }

                ReconcileTo(value);
                // Will set the account balance eventually
            }
        }

        /// <summary>
        /// Set the account balance without triggering reconciliation.
        /// </summary>
        /// <param name="value"></param>
        internal void SetAccountBalance(decimal value)
        {
            if (value == _accountBalance) { return; }
            _accountBalance = value;
            OnPropertyChanged("AccountBalance");
        }

        #endregion

        #region Common Serialization / Notification methods
        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(String name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        [Browsable(false)]
        public ExtensionDataObject ExtensionData { get; set; }
        #endregion

        public void Delete()
        {
            if (List != null) { List.Remove(this); }
        }

        /// <summary>
        /// Returns true if transaction is blank. Considered blank
        /// if no information is entered, except the date.
        /// </summary>
        public bool IsBlank
        {
            get 
            {
                return
                    _amount == 0m &&
                    _kind == TransactionKind.Normal &&
                    String.IsNullOrEmpty(_category) &&
                    String.IsNullOrEmpty(_comment);
            }
        }
    }

}
